SPDX-FileCopyrightText: 2025 Florian Dubois SPDX-FileCopyrightText: 2025 AlICe laboratory https://alicelab.be
SPDX-License-Identifier: GPL-3.0-or-later
Florian Dubois 23/01/2025 Blender 4.2.2
import bpy
import math
import random
import mathutilsfor obj in bpy.data.objects:
    if obj.type != "CAMERA":
        bpy.data.objects.remove(obj, do_unlink=True)
bpy.ops.outliner.orphans_purge()cube_size = 100
bpy.ops.mesh.primitive_cube_add(size=cube_size, location=(0, 0, 0))
cube = bpy.context.objectAppliquer un modificateur Wireframe
wireframe_mod = cube.modifiers.new(name="Wireframe", type="WIREFRAME")
wireframe_mod.use_replace = True
wireframe_mod.thickness = 1  # ajuster au besoindef random_unit_vector():
    theta = random.uniform(0, 2 * math.pi)  # Angle autour de l'axe Z
    phi = random.uniform(0, 2 * math.pi)  # Angle de l'axe Z vers l'axe Y
    x = math.sin(phi) * math.cos(theta)
    y = math.sin(phi) * math.sin(theta)
    z = math.cos(phi) * math.sin(theta)
    if durée == 5:
        y = 0
    elif durée % 5 == 0:
        x = 0
        y = 90 * math.pi / 180
    else:
        diff = durée
        while diff > 5:
            diff -= 5
        y = y * diff
        x = x * diff
        z = z * diff
    return mathutils.Vector((x, y, z)).normalized()Retourne la réflexion d’un vecteur par rapport à une normale.
def reflect_vector(vector, normal):    return vector - 2 * vector.dot(normal) * normalCalcule où un rayon (origin + t*direction) intersecte en premier le cube de demi-taille half_size.
def find_intersection(origin, direction, half_size):    epsilon = 1e-5
    scales = [
        (half_size - origin.x) / direction.x if direction.x != 0 else float("inf"),
        (-half_size - origin.x) / direction.x if direction.x != 0 else float("inf"),
        (half_size - origin.y) / direction.y if direction.y != 0 else float("inf"),
        (-half_size - origin.y) / direction.y if direction.y != 0 else float("inf"),
        (half_size - origin.z) / direction.z if direction.z != 0 else float("inf"),
        (-half_size - origin.z) / direction.z if direction.z != 0 else float("inf"),
    ]
    valid_scales = [s for s in scales if s > epsilon and s != float("inf")]
    if valid_scales:
        scale = min(valid_scales)
    else:Aucun scale strictement positif On peut choisir de ne pas créer de cylindre, ou de mettre scale=0
        scale = 0.0
    point = origin + direction * scaleClamp pour que le point reste bien dans la surface (à ± half_size)
    point.x = max(-half_size, min(half_size, point.x))
    point.y = max(-half_size, min(half_size, point.y))
    point.z = max(-half_size, min(half_size, point.z))
    return pointfraction_min = 0.4  # intensité=1 => 40% du diamètre max
fraction_max = 1.0  # intensité=8 => 100% du diamètre max
steps = 7  # (8 - 1)Calcule la fraction du diamètre max en fonction de l’intensité (1..8). intensité = 1 => fraction_min intensité = 8 => fraction_max
def fraction_for_intensity(i):    i_clamp = max(1, min(8, i))
    return fraction_min + (i_clamp - 1) * (fraction_max - fraction_min) / stepsvariables = [
    {"durée": 2, "répétition": True, "notes": 4, "type": "Sixteenth"},  # 0
    {"durée": 2, "répétition": True, "notes": 7, "type": "Sixteenth"},  # 1
    {"durée": 5, "répétition": False, "notes": 2, "type": "Long_held"},  # 2
    {"durée": 2, "répétition": True, "notes": 3, "type": "Sixteenth"},  # 3
    {"durée": 2, "répétition": True, "notes": 1, "type": "Sixteenth"},  # 4
    {"durée": 5, "répétition": False, "notes": 1, "type": "Long_held"},  # 5
    {"durée": 2, "répétition": True, "notes": 7, "type": "Sixteenth"},  # 6
    {"durée": 15, "répétition": False, "notes": 8, "type": "Long_staccato"},  # 7
    {"durée": 2, "répétition": True, "notes": 7, "type": "Sixteenth"},  # 8
    {"durée": 2, "répétition": True, "notes": 5, "type": "Sixteenth"},  # 9
    {"durée": 16, "répétition": False, "notes": 10, "type": "Long_staccato"},  # 10
    {"durée": 4, "répétition": True, "notes": 15, "type": "Sixteenth"},  # 11
    {"durée": 15, "répétition": False, "notes": 12, "type": "Long_staccato"},  # 12
    {"durée": 2, "répétition": True, "notes": 7, "type": "Sixteenth"},  # 13
    {"durée": 14, "répétition": False, "notes": 10, "type": "Long_staccato"},  # 14
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 15
    {"durée": 15, "répétition": False, "notes": 9, "type": "Long_staccato"},  # 16
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 17
    {"durée": 17, "répétition": False, "notes": 19, "type": "Long_staccato"},  # 18
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 19
    {"durée": 33, "répétition": False, "notes": 10, "type": "Long_held"},  # 20
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 21
    {"durée": 36, "répétition": False, "notes": 4, "type": "Long_held"},  # 22
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 23
    {"durée": 30, "répétition": False, "notes": 17, "type": "Long_staccato"},  # 24
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 25
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 26
    {"durée": 24, "répétition": False, "notes": 10, "type": "Long_held"},  # 27
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 28
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 29
    {"durée": 3, "répétition": True, "notes": 12, "type": "Sixteenth"},  # 30
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 31
    {"durée": 5, "répétition": False, "notes": 1, "type": "Long_staccato"},  # 32
    {"durée": 7, "répétition": False, "notes": 4, "type": "Long_staccato"},  # 33
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 34
    {"durée": 8, "répétition": False, "notes": 1, "type": "Long_held"},  # 35
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 36
    {"durée": 6, "répétition": True, "notes": 18, "type": "Sixteenth"},  # 37
    {"durée": 4, "répétition": True, "notes": 4, "type": "Sixteenth"},  # 38
    {"durée": 3, "répétition": True, "notes": 10, "type": "Sixteenth"},  # 39
    {"durée": 3, "répétition": False, "notes": 8, "type": "Sixteenth"},  # 40
    {"durée": 5, "répétition": True, "notes": 7, "type": "Sixteenth"},  # 41
    {"durée": 20, "répétition": False, "notes": 38, "type": "Sixteenth"},  # 42
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 43
    {"durée": 5, "répétition": False, "notes": 1, "type": "Long_staccato"},  # 44
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 45
    {"durée": 6, "répétition": True, "notes": 19, "type": "Sixteenth"},  # 46
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 47
    {"durée": 2, "répétition": True, "notes": 5, "type": "Sixteenth"},  # 48
    {"durée": 30, "répétition": False, "notes": 21, "type": "Long_staccato"},  # 49
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 50
    {"durée": 3, "répétition": True, "notes": 1, "type": "Sixteenth"},  # 51
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 52
    {"durée": 5, "répétition": False, "notes": 1, "type": "Long_staccato"},  # 53
    {"durée": 5, "répétition": True, "notes": 11, "type": "Sixteenth"},  # 54
    {"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 55
    {"durée": 5, "répétition": False, "notes": 1, "type": "Long_held"},  # 56
    {"durée": 4, "répétition": True, "notes": 11, "type": "Sixteenth"},  # 57
    {"durée": 2, "répétition": True, "notes": 1, "type": "Sixteenth"},  # 58
    {"durée": 3, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 59
    {"durée": 3, "répétition": True, "notes": 8, "type": "Sixteenth"},  # 60
    {"durée": 14, "répétition": False, "notes": 3, "type": "Long_held"},  # 61
]Patern_choisi = 11  # indice voulu (depuis la partition)
Patern_ciblé = Patern_choisi - 1
Patern_ciblé_biblio = Patern_ciblé
longueur_interval = 3Filtrer la bibliothèque : extraire 3 éléments autour du pattern
bibliothèque_filtrée = variables[Patern_ciblé_biblio - 1 : Patern_ciblé_biblio + 2]Pattern cible
pattern_cible = variables[Patern_ciblé]
rep = 1 if pattern_cible["répétition"] else 0patern_en_cours = 0
half_size = cube_size / 2
if rep == 0:
    for variable in bibliothèque_filtrée:
        patern_en_cours += 1
        notes = variable["notes"]
        durée = variable["durée"]
        répétitions = 1
        rebonds = 8Ajustements en fonction de patern_en_cours
        if variable["répétition"]:
            if patern_en_cours == 1:
                répétitions = 8
                rebonds = 0
            if patern_en_cours == 3:
                répétitions = 8
                rebonds = 8
        else:
            répétitions = 1
            rebonds = 8Calcul du diamètre de base en fonction du type
        if variable["type"] == "Sixteenth":
            initial_diameter = notes / durée * 3
        elif variable["type"] == "Long_staccato":
            initial_diameter = notes / durée * 50
        elif variable["type"] == "Long_held":
            initial_diameter = notes * 20
        else:
            initial_diameter = notesPréfix selon patern_en_cours
        if patern_en_cours == 1:
            prefix = "precedent_"
        elif patern_en_cours == 2:
            prefix = "cible_"
        elif patern_en_cours == 3:
            prefix = "suivant_"On génère la direction aléatoire 1 seule fois pour chaque pattern
        origin = mathutils.Vector((0, 0, 0))
        direction = random_unit_vector()
        for _ in range(répétitions):Ajuster rebonds
            if patern_en_cours == 3:
                rebonds -= 1
            if patern_en_cours == 1:
                rebonds += 1
            for rebond_index in range(rebonds):Calcul intensité courante
                if patern_en_cours == 1:
                    intensity_current = rebonds - rebond_index
                else:
                    intensity_current = max(8 - rebond_index, 1)Intensité suivante pour “rebond_index + 1”
                if rebond_index < rebonds - 1:
                    if patern_en_cours == 1:
                        intensity_next = rebonds - (rebond_index + 1)
                    else:
                        intensity_next = max(8 - (rebond_index + 1), 1)
                else:Dernier rebond => intensité identique ou au choix
                    intensity_next = intensity_currentConvertir intensités en diamètres
                diameter_current = (
                    fraction_for_intensity(intensity_current) * initial_diameter
                )
                diameter_next = (
                    fraction_for_intensity(intensity_next) * initial_diameter
                )Trouver point d’impact
                end_point = find_intersection(origin, direction, half_size)
                length = (end_point - origin).length
                location = (origin + end_point) / 2                bpy.ops.mesh.primitive_cone_add(
                    vertices=32,
                    radius1=diameter_current / 2,
                    radius2=diameter_next / 2,
                    depth=length,
                    location=location,
                )
                cone = bpy.context.object
                cone.name = f"{prefix}{cone.name}_rebond_{rebond_index}"Aligner le tronc de cône
                cone_vector = end_point - origin
                cone.rotation_mode = "QUATERNION"
                cone.rotation_quaternion = cone_vector.to_track_quat("Z", "Y")=== (B) CRÉER UNE SPHÈRE À LA JONCTION (end_point) === On lui donne le diamètre du “sommet” du tronçon (diameter_next).
                sphere_radius = diameter_next / 2
                bpy.ops.mesh.primitive_uv_sphere_add(
                    radius=sphere_radius, location=end_point
                )
                sphere = bpy.context.object
                sphere.name = f"{prefix}{sphere.name}_rebond_{rebond_index}"(Optionnel) On peut lui donner le même matériau que le cône
                if cone.data.materials:
                    sphere.data.materials.append(cone.data.materials[0])                normal = mathutils.Vector((0, 0, 0))
                eps = 1e-4
                if abs(end_point.x - half_size) < eps:
                    normal = mathutils.Vector((1, 0, 0))
                elif abs(end_point.x + half_size) < eps:
                    normal = mathutils.Vector((-1, 0, 0))
                elif abs(end_point.y - half_size) < eps:
                    normal = mathutils.Vector((0, 1, 0))
                elif abs(end_point.y + half_size) < eps:
                    normal = mathutils.Vector((0, -1, 0))
                elif abs(end_point.z - half_size) < eps:
                    normal = mathutils.Vector((0, 0, 1))
                elif abs(end_point.z + half_size) < eps:
                    normal = mathutils.Vector((0, 0, -1))
                direction = reflect_vector(direction, normal).normalized()
                origin = end_point
if rep == 1:
    for variable in bibliothèque_filtrée:
        patern_en_cours += 1
        notes = variable["notes"]
        durée = variable["durée"]
        répétitions = 16
        rebonds = 8Ajustements en fonction de patern_en_cours
        if variable["répétition"]:
            if patern_en_cours == 1:
                répétitions = 8
                rebonds = 0
            if patern_en_cours == 3:
                répétitions = 8
                rebonds = 8
        else:
            répétitions = 1
            rebonds = 8Calcul du diamètre de base en fonction du type
        if variable["type"] == "Sixteenth":
            initial_diameter = notes / durée * 2
        elif variable["type"] == "Long_staccato":
            initial_diameter = notes / durée * 100
        elif variable["type"] == "Long_held":
            initial_diameter = notes * 10
        else:
            initial_diameter = notesPréfix selon patern_en_cours
        if patern_en_cours == 1:
            prefix = "precedent_"
        elif patern_en_cours == 2:
            prefix = "cible_"
        elif patern_en_cours == 3:
            prefix = "suivant_"On génère la direction aléatoire 1 seule fois pour chaque pattern
        origin = mathutils.Vector((0, 0, 0))
        direction = random_unit_vector()
        for _ in range(répétitions):Ajuster rebonds
            if patern_en_cours == 3:
                rebonds -= 1
            if patern_en_cours == 1:
                rebonds += 1
            for rebond_index in range(rebonds):Calcul intensité courante
                if patern_en_cours == 1:
                    intensity_current = rebonds - rebond_index
                else:
                    intensity_current = max(8 - rebond_index, 1)Intensité suivante pour “rebond_index + 1”
                if rebond_index < rebonds - 1:
                    if patern_en_cours == 1:
                        intensity_next = rebonds - (rebond_index + 1)
                    else:
                        intensity_next = max(8 - (rebond_index + 1), 1)
                else:Dernier rebond => intensité identique ou au choix
                    intensity_next = intensity_currentConvertir intensités en diamètres
                diameter_current = (
                    fraction_for_intensity(intensity_current) * initial_diameter
                )
                diameter_next = (
                    fraction_for_intensity(intensity_next) * initial_diameter
                )Trouver point d’impact
                end_point = find_intersection(origin, direction, half_size)
                length = (end_point - origin).length
                location = (origin + end_point) / 2                bpy.ops.mesh.primitive_cone_add(
                    vertices=32,
                    radius1=diameter_current / 2,
                    radius2=diameter_next / 2,
                    depth=length,
                    location=location,
                )
                cone = bpy.context.object
                cone.name = f"{prefix}{cone.name}_rebond_{rebond_index}"Aligner le tronc de cône
                cone_vector = end_point - origin
                cone.rotation_mode = "QUATERNION"
                cone.rotation_quaternion = cone_vector.to_track_quat("Z", "Y")=== (B) CRÉER UNE SPHÈRE À LA JONCTION (end_point) === On lui donne le diamètre du “sommet” du tronçon (diameter_next).
                sphere_radius = diameter_next / 2
                bpy.ops.mesh.primitive_uv_sphere_add(
                    radius=sphere_radius, location=end_point
                )
                sphere = bpy.context.object
                sphere.name = f"{prefix}{sphere.name}_rebond_{rebond_index}"(Optionnel) On peut lui donner le même matériau que le cône
                if cone.data.materials:
                    sphere.data.materials.append(cone.data.materials[0])                normal = mathutils.Vector((0, 0, 0))
                eps = 1e-4
                if abs(end_point.x - half_size) < eps:
                    normal = mathutils.Vector((1, 0, 0))
                elif abs(end_point.x + half_size) < eps:
                    normal = mathutils.Vector((-1, 0, 0))
                elif abs(end_point.y - half_size) < eps:
                    normal = mathutils.Vector((0, 1, 0))
                elif abs(end_point.y + half_size) < eps:
                    normal = mathutils.Vector((0, -1, 0))
                elif abs(end_point.z - half_size) < eps:
                    normal = mathutils.Vector((0, 0, 1))
                elif abs(end_point.z + half_size) < eps:
                    normal = mathutils.Vector((0, 0, -1))
                direction = reflect_vector(direction, normal).normalized()
                origin = end_pointCrée et assigne un matériau avec une couleur donnée.
def assign_material(obj, color):    if obj.type == "MESH":
        mat = bpy.data.materials.new(name=f"Material_{color}")
        mat.use_nodes = True
        nodes = mat.node_tree.nodes
        links = mat.node_tree.linksEffacer les noeuds existants
        for node in nodes:
            nodes.remove(node)Principled BSDF + Material Output
        principled_node = nodes.new(type="ShaderNodeBsdfPrincipled")
        output_node = nodes.new(type="ShaderNodeOutputMaterial")
        links.new(principled_node.outputs["BSDF"], output_node.inputs["Surface"])Couleur de base
        principled_node.inputs["Base Color"].default_value = (*color, 1)Assigner le matériau à l’objet
        if len(obj.data.materials) == 0:
            obj.data.materials.append(mat)
        else:
            obj.data.materials[0] = mat
color_mapping = {
    "cible": (0.8, 0.40, 0.40),  # Vert
    "precedent": (0.8, 0.60, 0.45),  # Bleu
    "suivant": (0.95, 0.60, 0.30),  # Rouge
    "Cube": (0, 0, 0),  # Noir
}Appliquer les couleurs aux objets correspondants
bpy.ops.object.select_all(action="DESELECT")
for category, color in color_mapping.items():
    bpy.ops.object.select_all(action="DESELECT")
    for obj in bpy.data.objects:
        if obj.name.startswith(category):
            obj.select_set(True)
    for obj in bpy.context.selected_objects:
        assign_material(obj, color)